#include "paragraph.h"

#include <list>
#include <stack>

#include <include/core/SkPaint.h>
#include <include/core/SkFont.h>
#include <include/core/SkFontMgr.h>
#include <include/core/SkCanvas.h>
#include <include/core/SkTextBlob.h>
#include <hb.h>
#include <hb-icu.h>
#include <unicode/ubidi.h>
#include <unicode/utf8.h>
#include <unicode/utf16.h>
#include <scrptrun.h>

#include "typeface_getter.h"

#define DEBUG_CONTINUE(fmt, ...) if (debug) { printf(fmt, ##__VA_ARGS__); }
#define DEBUG(fmt, ...) DEBUG_CONTINUE(fmt "\n", ##__VA_ARGS__)

namespace {
hb_blob_t *HbFaceReferenceTableSkTypeface(hb_face_t *face, hb_tag_t tag, void *context)
{
    SkTypeface* typeface = reinterpret_cast<SkTypeface*>(context);
    if (typeface == nullptr) {
        return nullptr;
    }

    const size_t tableSize = typeface->getTableSize(tag);
    if (tableSize == 0) {
        return nullptr;
    }

    void* buffer = malloc(tableSize);
    if (buffer == nullptr) {
        return nullptr;
    }

    size_t actualSize = typeface->getTableData(tag, 0, tableSize, buffer);
    if (tableSize != actualSize) {
        free(buffer);
        return nullptr;
    }

    return hb_blob_create(reinterpret_cast<char *>(buffer),
                          tableSize, HB_MEMORY_MODE_WRITABLE, buffer, free);
}
} // namespace

void Paragraph::SetText(const std::string &text)
{
    text_ = text;
}

void Paragraph::Paint(SkCanvas &canvas)
{
    // debug info
    bool debug = false;
    if (lastText_ != text_) {
        lastText_ = text_;
        debug = true;
    }
    DEBUG("Paint New Test: '%s' {", lastText_.c_str());

    TypefaceGetter getter;
    std::vector<Typeface> tfs;

    getter.GetByFamilies("Droid Sans,Source Code Pro,Noto Sans Arabic,Noto Color Emoji,Standard Symbols PS,Noto Sans Symbols2,Droid Sans Fallback,san-serif,serif", {}, tfs);
    DEBUG("  found typeface");
    int32_t index = 0;
    for (auto &tf : tfs) {
        DEBUG("    %d: %s {em: %d}", index++,
              tf.GetPostScriptName().c_str(), tf.Get()->getUnitsPerEm());
    }

    DEBUG("  utf8 -> utf16");
    std::vector<uint16_t> u16text;
    index = 0;
    {
        int32_t i = 0;
        uint32_t cp = 0;
        const int32_t textLength = text_.size();
        while (i < textLength) {
            U8_NEXT(text_.c_str(), i, textLength, cp);
            DEBUG("    %2d: {%d, 0x%06X}", index++, i, cp);
            if (U16_LENGTH(cp) == 1) {
                u16text.push_back(cp);
            } else {
                u16text.push_back(U16_LEAD(cp));
                u16text.push_back(U16_TRAIL(cp));
            }
        }
    }

    DEBUG("  bidi");
    struct ScriptedRun {
        int32_t start;
        int32_t end;
        UScriptCode script;
        bool rtl;
        sk_sp<SkTypeface> typeface;
    };
    std::list<struct ScriptedRun> runs;
    auto bidi = ubidi_open();
    if (bidi) {
        auto status = U_ZERO_ERROR;
        ubidi_setPara(bidi, reinterpret_cast<UChar *>(u16text.data()),
                      u16text.size(), UBIDI_DEFAULT_LTR, nullptr, &status);
        if (U_SUCCESS(status)) {
            auto size = ubidi_countRuns(bidi, &status);
            for (auto i = 0; i < size; ++i) {
                int start = -1;
                int length = -1;
                auto runDir = ubidi_getVisualRun(bidi, i, &start, &length);
                auto rtl = runDir == UBIDI_RTL;
                icu::ScriptRun scriptRun(reinterpret_cast<UChar *>(u16text.data()), start, length);
                std::stack<struct ScriptedRun> rtlStack;
                while (scriptRun.next()) {
                    if (rtl) {
                        rtlStack.push({scriptRun.getScriptStart(),
                                       scriptRun.getScriptEnd(),
                                       scriptRun.getScriptCode(),
                                       true});
                    } else {
                        runs.push_back({scriptRun.getScriptStart(),
                                        scriptRun.getScriptEnd(),
                                        scriptRun.getScriptCode(),
                                        false});
                        DEBUG("    [%d, %d): %s", runs.back().start
                                                , runs.back().end
                                                , uscript_getName(runs.back().script));
                    }
                }

                if (rtl) {
                    while (!rtlStack.empty()) {
                        runs.push_back(rtlStack.top());
                        DEBUG("    [%d, %d): %s", runs.back().start
                                                , runs.back().end
                                                , uscript_getName(runs.back().script));
                        rtlStack.pop();
                    }
                }
            }
        }
    }

    DEBUG("  find typeface");
    index = 0;
    for (auto it = runs.begin(); it != runs.end(); it++) {
        DEBUG("    [%d, %d)", it->start, it->end);
        auto ri = it->start;
        uint32_t cp = 0;
        Typeface *lastTypeface = nullptr;
        while (ri < it->end) {
            U16_NEXT(u16text.data(), ri, it->end, cp);
            DEBUG_CONTINUE("      [%02d: 0x%06X] ", index++, cp);

            if (u_hasBinaryProperty(cp, UCHAR_EMOJI)) {
                DEBUG_CONTINUE(" emoji ");
            }
            if (u_hasBinaryProperty(cp, UCHAR_EMOJI_MODIFIER)) {
                DEBUG_CONTINUE(" emoji-modifier ");
            }
            if (u_hasBinaryProperty(cp, UCHAR_EMOJI_MODIFIER_BASE)) {
                DEBUG_CONTINUE(" emoji-modifier-base ");
            }
            if (u_hasBinaryProperty(cp, UCHAR_EMOJI_COMPONENT)) {
                DEBUG_CONTINUE(" emoji-component ");
            }
            if (u_hasBinaryProperty(cp, UCHAR_EMOJI_PRESENTATION)) {
                DEBUG_CONTINUE(" emoji-presentation ");
            }
            if (u_hasBinaryProperty(cp, UCHAR_EMOJI_KEYCAP_SEQUENCE)) {
                DEBUG_CONTINUE(" emoji-keycap-sequence ");
            }
            if (cp == 0x1F3FF || cp == 0x1F468 || cp == 0x1F9B2) {
                DEBUG("special cached");
                continue;
            }

            if (lastTypeface && lastTypeface->Has(cp)) {
                DEBUG("cached");
                continue;
            }

            lastTypeface = nullptr;
            for (auto &tf : tfs) {
                if (tf.Has(cp)) {
                    lastTypeface = &tf;
                    break;
                }
            }

            if (it->typeface) {
                index--;
                DEBUG("new");
                auto next = it;
                runs.insert(++next, {
                    .start = ri - U16_LENGTH(cp),
                    .end = it->end,
                    .script = it->script,
                    .rtl = it->rtl,
                    .typeface = nullptr,
                });
                it->end = ri - U16_LENGTH(cp);
                break;
            }

            if (lastTypeface == nullptr) {
                DEBUG("no typeface");
                continue;
            }

            DEBUG("found at %s", lastTypeface->GetPostScriptName().c_str());
            it->typeface = lastTypeface->Get();
        }
    }

    DEBUG("  emoji control");
    for (auto it = runs.begin(); it != runs.end(); it++) {
        auto ri = it->start;
        uint16_t cp = 0;
        while (ri < it->end) {
            U16_NEXT(u16text.data(), ri, it->end, cp);
            if (cp == 0xFE0F && ri - U16_LENGTH(cp) == it->start) {
                it->start--;
                it--;
                it->end--;
                it++;
            }
        }
        DEBUG("    [%d, %d)", it->start, it->end);
    }

    DEBUG("  shape");
    index = 0;
    auto x = 0.0;
    for (auto &run : runs) {
        DEBUG("    [%d, %d)", run.start, run.end);
        SkFont font;
        sk_sp<SkTypeface> typeface = nullptr;
        if (run.typeface != nullptr) {
            typeface = run.typeface;
        } else {
            typeface = SkTypeface::MakeFromFile("out/DroidSansFallback.ttf");
        }
        font.setTypeface(typeface);
        constexpr auto size = 60;
        font.setSize(size);

        auto hbuffer = hb_buffer_create();
        hb_buffer_add_utf16(hbuffer, reinterpret_cast<const uint16_t *>(u16text.data()), -1, run.start, run.end - run.start);
        hb_buffer_set_direction(hbuffer, run.rtl ? HB_DIRECTION_RTL : HB_DIRECTION_LTR);
        hb_buffer_set_script(hbuffer, hb_icu_script_to_script(run.script));
        hb_buffer_set_language(hbuffer, hb_language_from_string("zh", -1));

        auto hface = hb_face_create_for_tables(HbFaceReferenceTableSkTypeface, font.getTypeface(), 0);
        auto hfont = hb_font_create(hface);
        hb_shape(hfont, hbuffer, nullptr, 0); // TODO: true features
        auto ng = 0u;
        auto hginfos = hb_buffer_get_glyph_infos(hbuffer, &ng);
        auto hgpositions = hb_buffer_get_glyph_positions(hbuffer, nullptr);

        char u8str[8] = {'\''};
        int cp = 0;
        size_t ri = 0;
        for (auto i = 0u; i < ng; i++) {
            U16_NEXT(u16text.data() + run.start, ri, u16text.size() - run.start, cp);
            int u8index = 1;
            int error = 0;
            U8_APPEND(u8str, u8index, sizeof(u8str), cp, error);
            u8str[u8index++] = '\'';
            u8str[u8index++] = 0;
            DEBUG("      %02d: %-4s (char: 0x%06X, codepoint: 0x%04X, cluster: %2u) {%.3lf, %.3lf, %.2lf, %.2lf}",
                  index++, u8str, u16text[i + run.start], hginfos[i].codepoint, hginfos[i].cluster,
                  1.0 * size * hgpositions[i].x_advance / typeface->getUnitsPerEm(),
                  1.0 * size * hgpositions[i].y_advance / typeface->getUnitsPerEm(),
                  1.0 * size * hgpositions[i].x_offset / typeface->getUnitsPerEm(),
                  1.0 * size * hgpositions[i].y_offset / typeface->getUnitsPerEm());
        }
        index += run.end - run.start - ng;

        SkTextBlobBuilder builder;
        auto blob = builder.allocRunPos(font, ng);
        for (auto i = 0u; i < ng; i++) {
            blob.glyphs[i] = hginfos[i].codepoint;
            blob.pos[i * 2 + 0] = x;
            blob.pos[i * 2 + 1] = 0;
            x += size * (hgpositions[i].x_advance  + hgpositions[i].x_offset) / typeface->getUnitsPerEm();
        }

        hb_buffer_destroy(hbuffer);
        hb_font_destroy(hfont);
        hb_face_destroy(hface);

        SkPaint paint;
        paint.setAntiAlias(true);
        paint.setARGB(255, 255, 0, 0);
        canvas.drawTextBlob(builder.make(), 100, 100, paint);
    }

    DEBUG("}");
}
